"use strict"

module.exports = function(options){
  let template   = require("es6-template-strings")
    ,_           = require("lodash")
    ,webRequest  = require('request')
    ,log4js      = require('log4js')
    ,logger      = log4js.getLogger()
    ,debug       = require('debug')("http")
    ,rp          = require("request-promise");

  const GET_ACCESS_TOKEN_URL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?"
                                    + "corpid=${corpId}&corpsecret=${corpSecret}";

  var QywxAccessTokenClient = function(corpId, corpSecret, models){
    if( !(this instanceof QywxAccessTokenClient) ){
      return new QywxAccessTokenClient(options);
    }else{
      this.corpId     = corpId;
      this.corpSecret = corpSecret;
      this.models     = models;

      this.targetUrl =
            template(GET_ACCESS_TOKEN_URL, { corpId: this.corpId, corpSecret: this.corpSecret });
    }
  }

  QywxAccessTokenClient.prototype.get = function(cb){
    var client = this;
    var QywxAccessToken = this.models.QywxAccessToken;

    var rpOptions = {
        uri: client.targetUrl,
        qs: {
            //access_token: 'xxxxx xxxxx' // -> uri + '?access_token=xxxxx%20xxxxx'
        },
        headers: {
            //'User-Agent': 'Request-Promise'
        },
        json: true // Automatically parses the JSON string in the response
    };
    var query = QywxAccessToken.findOne({ corpId: client.corpId });
    query.then(function(accessToken){
      if(accessToken === null){
        //Try to get one
        return rp(rpOptions).then(function(data){
            return QywxAccessToken.findOneAndUpdate({
                corpId: client.corpId
            }, {
              corpId: client.corpId,
              accessToken: data.access_token,
              expiresAt: _.now() + (data.expires_in - 118 * 60) * 1000,
              lockedAt: null,
              lockedForRefresh: false
            }, {
              new: true,
              upsert: true
            });
        }).then(function(data){ //Get the final accessToken
          return cb(null, data);
        });
      }else{
        debug("accessToken already exists: ", accessToken);
        if( accessToken.expiresAt > _.now() ){
          //Just use the accessToken
          return cb(null, accessToken);
        }else{
          //Token expired we need to retrieve it
          if( !accessToken.lockedForRefresh || accessToken.lockedAt < _.now() - 2 * 60 * 1000 ){
            QywxAccessToken.findOneAndUpdate(
                {
                  corpId: client.corpId
                  ,accessToken: accessToken.accessToken
                  ,$or: [{
                    lockedForRefresh: false
                  }, {
                    lockedForRefresh: true,
                    lockedAt : {
                      $lt: _.now() - 2 * 60 * 1000  //Locked too long, we need unlock it to prevent deadlock
                    }
                  }]
                }, { lockedForRefresh: true, lockedAt: _.now() }
              )
              .then(function(instance){
                //Try to retrieve new Token
                return rp(rpOptions).then(function(data){
                    return QywxAccessToken.findOneAndUpdate({
                        corpId: client.corpId
                    }, {
                      corpId: client.corpId,
                      accessToken: data.access_token,
                      expiresAt: _.now() + (data.expires_in - 5 * 60) * 1000,
                      lockedAt: null,
                      lockedForRefresh: false
                    }, {
                      new: true,
                      upsert: true
                    });
                }).then(function(data){ //Get the final accessToken
                  return cb(null, data);
                });
              })
              .catch(function(err){
                logger.error(err);
                return cb(err);
              });
          }else{
            //Token are locked by another thread, we just pass on
            debug("token are refreshing by another thread, keep going");
            return cb(null, accessToken);;
          }
        }
      }
    })
    .catch(function(err){
      return cb(err);
    });
  }

  return new QywxAccessTokenClient(options.corpId, options.corpSecret, options.models);
}
